window.Onpage_Help = function (options) {
    var $ = window.jQuery || null;
    if ($ == null) return;

    options = options || {}
    var defaults = {
        include_all: true,
        icon_1: 'fa fa-question',
        icon_2: 'fa fa-lightbulb-o',
        base: '',
        code_highlight: (!!window.Rainbow ? 'rainbow' : (!!window.Prism ? 'prism' : null)),

        add_panels: true,
        panel_content_selector: '.info-section',
        panel_content_title: '.info-title'
    }
    this.settings = $.extend({}, defaults, options);


    var $base = this.settings['base'];
    var ie_fix = document.all && !window.atob;//ie9 and below need a little fix

    var section_start = {};
    var section_end = {};
    var section_rect = {};
    var section_count = 0;

    var created = false;
    var active = false;

    var self = this, _ = this;
    var ovfx = '';
    var help_container = null;

    var body_h, body_w;

    var captureFocus = function () {
        if (!help_container) return;
        var scroll = -1;
        //like bootstrap modal
        $(document)
            .off('focusin.ace.help') //remove any previously attached handler
            .on('focusin.ace.help', function (e) {
                if (!(help_container[0] == e.target || $.contains(help_container[0], e.target))) {
                    help_container.focus();
                }

                if (e.target == document && scroll > -1) {
                    //when window regains focus and container is focused, it scrolls to bottom
                    //so we put it back to its place
                    $('body,html').scrollTop(scroll);
                    scroll = -1;
                }
            })

        $(window).on('blur.ace.help', function () {
            scroll = $(window).scrollTop();
        });
    }
    var releaseFocus = function () {
        $(document).off('focusin.ace.help');
        $(window).off('blur.ace.help');
    }


    this.toggle = function () {
        if (active) {
            self.disable();
        } else {
            self.enable();
        }
    }

    this.enable = function () {
        if (active) return;
        if (typeof _.settings.before_enable === 'function' && _.settings.before_enable.call(self) === false) return;
        ////

        //if( !created ) this.init();
        active = true;

        $('.onpage-help-backdrop, .onpage-help-section').removeClass('hidden');

        ovfx = document.body.style.overflowX;
        document.body.style.overflowX = 'hidden';//hide body:overflow-x

        display_help_sections();
        captureFocus();

        ////
        if (typeof _.settings.after_enable === 'function') _.settings.after_enable.call(self);
    }

    this.disable = function () {
        if (!active) return;
        if (typeof _.settings.before_disable === 'function' && _.settings.before_disable.call(self)) return;
        ////

        active = false;
        $('.onpage-help-backdrop, .onpage-help-section').addClass('hidden');

        document.body.style.overflowX = ovfx;//restore body:overflow-x
        releaseFocus();

        ////
        if (typeof _.settings.after_disable === 'function') _.settings.after_disable.call(self);
    }

    this.is_active = function () {
        return active;
    }
    this.show_section_help = function (section) {
        launch_help_modal(section, true);
    }


    this.init = function () {
        if (created) return;

        help_container =
            $('<div class="onpage-help-container" id="onpage-help-container" tabindex="-1" />')
                .appendTo('body');

        help_container.append('<div class="onpage-help-backdrop hidden" />')

        //update to correct position and size
        $(window).on('resize.onpage_help', function () {
            if (!active) return;
            display_help_sections();

            if (help_modal != null && help_modal.hasClass('in')) {
                setBodyHeight();
                disableBodyScroll();
            }
        })

        created = true;
    }
    this.init();//create once at first


    ///////////////////////////
    this.update_sections = function () {
        save_sections(true);//reset sections, maybe because of new elements and comments inserted into DOM
    }


    function display_help_sections() {
        if (!active) return;

        save_sections();//finds comments and relevant help sections

        body_h = document.body.scrollHeight - 2;
        body_w = document.body.scrollWidth - 2;

        //we first calculate all positions
        //because if we calculate one position and then make changes to DOM,
        //next position calculation will become slow on Webkit, because it tries to re-calculate layout changes and things
        //i.e. we batch call all and save offsets and scrollWidth, etc and then use them later in highlight_section
        //Firefox doesn't have such issue
        for (var name in section_start) {
            if (section_start.hasOwnProperty(name)) {
                save_section_offset(name);
            }
        }
        for (var name in section_start) {
            if (section_start.hasOwnProperty(name)) {
                highlight_section(name);
            }
        }
    }


    //finds comments and relevant help sections
    function save_sections(reset) {
        if (!(reset === true || section_count == 0)) return;//no need to re-calculate sections, then return
        if (reset === true) help_container.find('.onpage-help-section').remove();

        section_start = {};
        section_end = {};
        section_count = 0;

        var count1 = 0, count2 = 0;

        //find all relevant comments
        var comments = $('*').contents().filter(function () {
            return this.nodeType == 8
            /**Node.COMMENT_NODE;*/
        })
        $(comments).each(function () {
            var match
            if ((match = $.trim(this.data).match(/#section\s*:\s*([\w\d\-\.\/]+)/i))) {
                var section_name = match[1];
                if (!(section_name in section_start)) section_start[section_name] = this;
            }
            if ((match = $.trim(this.data).match(/\/section\s*:\s*([\w\d\-\.\/]+)/i))) {
                var section_name = match[1];
                if (!(section_name in section_end) && (section_name in section_start)) {
                    section_end[section_name] = this;
                    section_count++;
                }
            }
        })
    }


    function save_section_offset(name) {
        if (!(name in section_start) || !(name in section_end)) return;

        var x1 = 1000000, y1 = 1000000, x2 = -1000000, y2 = -1000000;
        var visible = false;


        var elements = [];

        var start = section_start[name];
        var end = section_end[name];
        while (start != end) {
            start = start.nextSibling;
            if (start == null) break;
            else if (start.nodeType == 1 /**Node.ELEMENT_NODE*/) elements.push(start);
        }

        var elen = elements.length;
        if (elen > 0 && !_.settings['include_all']) {
            //calculate dimension of only first and last element
            elements = elen == 1 ? [elements[0]] : [elements[0], elements[elen - 1]]
        }

        $(elements).each(function () {
            var $this = $(this);
            if ($this.is(':hidden')) return;

            var off = $this.offset();
            var w = $this.outerWidth();
            var h = $this.outerHeight();

            if (!off || !w || !h) return;

            visible = true;
            if (off.left < x1) x1 = off.left;
            if (off.left + w > x2) x2 = off.left + w;

            if (off.top < y1) y1 = off.top;
            if (off.top + h > y2) y2 = off.top + h;
        });


        if (!visible) {
            section_rect[name] = {is_hidden: true}
            return;
        }

        x1 -= 1;
        y1 -= 1;
        x2 += 1;
        y2 += 1;


        var width = x2 - x1, height = y2 - y1;
        //section_rect is out of window ???
        if (x1 + width < 2 || x1 > body_w || y1 + height < 2 || y1 > body_h) {
            section_rect[name] = {is_hidden: true}
            return;
        }

        section_rect[name] = {
            'left': parseInt(x1),
            'top': parseInt(y1),
            'width': parseInt(width),
            'height': parseInt(height)
        }
    }


    function highlight_section(name) {
        if (!(name in section_rect) || !help_container) return;

        //div is the highlighted box above each section
        var div = help_container.find('.onpage-help-section[data-section="' + name + '"]').eq(0);
        if (div.length == 0) {
            div = $('<a class="onpage-help-section" href="#" />').appendTo(help_container);
            if (ie_fix) div.append('<span class="ie-hover-fix" />');

            if (_.settings.icon_1) div.append('<i class="help-icon-1 ' + _.settings.icon_1 + '"></i>');
            if (_.settings.icon_2) div.append('<i class="help-icon-2 ' + _.settings.icon_2 + '"></i>');

            div.attr('data-section', name);

            div.on('click', function (e) {
                e.preventDefault();
                launch_help_modal(name);
            });
        }

        var rect = section_rect[name];
        if (rect['is_hidden'] === true) {
            div.addClass('hidden');
            return;
        }

        div.css({
            left: rect.left,
            top: rect.top,
            width: rect.width,
            height: rect.height
        });


        div.removeClass('hidden');
        div.removeClass('help-section-small help-section-smaller');
        if (rect.height < 55 || rect.width < 55) {
            div.addClass('help-section-smaller');
        } else if (rect.height < 75 || rect.width < 75) {
            div.addClass('help-section-small');
        }
    }


    var nav_list = [];
    var nav_pos = -1;
    var mbody = null;
    var maxh = 0;
    var help_modal = null;

    //disable body scroll, when modal content has no scrollbars or reached end of scrolling
    function disableBodyScroll() {
        if (!mbody) return;

        var body = mbody[0];
        var disableScroll = body.scrollHeight <= body.clientHeight;

        //mousewheel library available?
        var mousewheel_event = !!$.event.special.mousewheel ? 'mousewheel.ace.help' : 'mousewheel.ace.help DOMMouseScroll.ace.help';

        mbody.parent()
            .off(mousewheel_event)
            .on(mousewheel_event, function (event) {
                if (disableScroll) event.preventDefault();
                else {
                    event.deltaY = event.deltaY || 0;
                    var delta = (event.deltaY > 0 || event.originalEvent.detail < 0 || event.originalEvent.wheelDelta > 0) ? 1 : -1

                    if (delta == -1 && body.scrollTop + body.clientHeight >= body.scrollHeight) event.preventDefault();
                    else if (delta == 1 && body.scrollTop <= 0) event.preventDefault();
                }
            });
    }

    function setBodyHeight() {
        if (!mbody) return;

        var diff = parseInt(help_modal.find('.modal-dialog').css('margin-top'));
        diff = diff + 110 + parseInt(diff / 2);
        maxh = parseInt($(window).innerHeight() - diff + 40);
        mbody.css({'max-height': maxh});
    }


    function launch_help_modal(section_name, save_to_list) {
        if (help_modal == null) {
            help_modal = $('<div id="onpage-help-modal" class="modal onpage-help-modal" tabindex="-1" role="dialog" aria-labelledby="HelpModalDialog" aria-hidden="true">\
			  <div class="modal-dialog modal-lg">\
				<div class="modal-content">\
					<div class="modal-header">\
					  <div class="pull-right onpage-help-modal-buttons">\
						<button aria-hidden="true" data-navdir="up" type="button" class="disabled btn btn-white btn-success btn-sm"><i class="ace-icon fa fa-level-up fa-flip-horizontal bigger-125 icon-only"></i></button>\
						&nbsp;\
						<button aria-hidden="true" data-navdir="back" type="button" class="disabled btn btn-white btn-info btn-sm"><i class="ace-icon fa fa-arrow-left icon-only"></i></button>\
						<button aria-hidden="true" data-navdir="forward" type="button" class="disabled btn btn-white btn-info  btn-sm"><i class="ace-icon fa fa-arrow-right icon-only"></i></button>\
						&nbsp;\
						<button aria-hidden="true" data-dismiss="modal" class="btn btn-white btn-danger btn-sm" type="button"><i class="ace-icon fa fa-times icon-only"></i></button>\
					  </div>\
					  <h4 class="modal-title">Help Dialog</h4>\
					</div>\
					<div class="modal-body"><div class="onpage-help-content"></div></div>\
				</div>\
			  </div>\
			</div>').appendTo('body');

            mbody = help_modal.find('.modal-body');
            mbody.css({'overflow-y': 'auto', 'overflow-x': 'hidden'});

            help_modal.css({'overflow': 'hidden'})
                .on('show.bs.modal', function () {
                    releaseFocus();
                })
                .on('hidden.bs.modal', function () {
                    captureFocus();
                })

            help_modal.find('.onpage-help-modal-buttons').on('click', 'button[data-navdir]', function () {
                var dir = $(this).attr('data-navdir');
                if (dir == 'back') {
                    if (nav_pos > 0) {
                        nav_pos--;
                        launch_help_modal(nav_list[nav_pos], false);
                    }
                } else if (dir == 'forward') {
                    if (nav_pos < nav_list.length - 1) {
                        nav_pos++;
                        launch_help_modal(nav_list[nav_pos], false);//don't save to history list, already in the list
                    }
                } else if (dir == 'up') {
                    var $this = $(this), url;
                    if ($this.hasClass('disabled') || !(url = $this.attr('data-url'))) return;

                    launch_help_modal(url, true);//add to history list
                }
            });
        }


        if (!help_modal.hasClass('in')) {
            if (document.body.lastChild != help_modal[0]) $(document.body).append(help_modal);//move it to become the last child of body
            help_modal.modal('show');

            setBodyHeight();
        }

        help_modal.find('.modal-title').wrapInner("<span class='hidden' />").append('<i class="fa fa-spinner fa-spin blue bigger-125"></i>');
        var content = $('.onpage-help-content');
        content.addClass('hidden')

        $(document.body).removeClass('modal-open');//modal by default hides body scrollbars, but we don't want to do so, because on modal hide, a winow resize is triggered

        var parts = section_name.match(/file\:(.*?)\:(.+)/i);
        if (parts && parts.length == 3) {
            display_codeview(parts[2], parts[1], false);
            return;
        }

        section_name = section_name.replace(/^#/g, '');
        if (typeof _.settings.section_url === 'function') url = _.settings.section_url.call(self, section_name);

        $.ajax({url: url, dataType: 'text'})
            .done(function (result) {
                //find the title for this dialog by looking for a tag that has data-id attribute
                var title = '', excerpt = '';

                if (typeof _.settings.section_title === 'function') title = _.settings.section_title.call(self, result, section_name, url);
                else {
                    var escapeSpecialChars = function (name) {
                        return name.replace(/[\-\.\(\)\=\"\'\\\/]/g, function (a, b) {
                            return "\\" + a;
                        })
                    }
                    var tname = section_name;
                    while (title.length == 0) {
                        var reg_str = '\\<([a-z][a-z0-9]*)(?:\\s+)(?:[^\\<\\>]+?)data\\-id\\=\\"\\#' + escapeSpecialChars(tname) + '\\"(?:[^\\>]*)\\>([\\s\\S]*?)</\\1>';

                        var regexp = new RegExp(reg_str, "im");
                        var arr = result.match(reg_str);
                        if (arr && arr[2]) {
                            title = arr[2];
                            break;
                        }

                        //if no "#something.part" was not found try looking for "#something" instead
                        var tpos
                        if ((tpos = tname.lastIndexOf('.')) > -1) {
                            tname = tname.substr(0, tpos);
                        } else break;
                    }
                }

                help_modal.find('.modal-title').html($.trim(title) || '&nbsp;');

                if (typeof _.settings.section_content === 'function') excerpt = _.settings.section_content.call(self, result, section_name, url);
                else {
                    var find1 = '<!-- #section:' + section_name + ' -->';
                    var pos1 = result.indexOf(find1);
                    var pos2 = result.indexOf('<!-- /section:' + section_name + ' -->', pos1);

                    if (pos1 == -1 || pos2 == -1) {
                        help_modal.find('.modal-title').html('&nbsp;');
                        return;
                    }

                    excerpt = result.substring(pos1 + find1.length + 1, pos2);
                }


                //convert `<` and `>` to `&lt;` and `&gt;` inside code snippets
                if (typeof _.settings.code_highlight === 'function') {
                    excerpt = _.settings.code_highlight.call(self, excerpt);
                } else {
                    //find prism & rainbow style pre tags and replace < > characters with &lt; &gt;
                    excerpt =
                        excerpt.replace(/\<pre((?:(?:.*?)(?:data\-language=["'](?:[\w\d]+)["'])(?:.*?))|(?:(?:.*?)(?:class=["'](?:.*?)language\-(?:[\w\d]+)(?:.*?)["'])(?:.*?)))\>([\s\S]+?)\<\/pre\>/ig, function (a, b, c) {
                            return '<pre' + (b) + '>' + c.replace(/\</g, '&lt;').replace(/\>/g, '&gt;') + '</pre>';
                        });
                }


                //modify image paths if needed!
                if (typeof _.settings.img_url === 'function') {
                    excerpt = excerpt.replace(/\<img(?:(?:.*?)src=["']([^"']+)["'])/ig, function (img, src) {
                        var new_src = _.settings.img_url.call(self, src);
                        return img.replace(src, new_src)
                    });
                }


                //now update content area
                content.empty().append(excerpt);
                if (typeof _.settings.code_highlight === 'function') {
                    _.settings.code_highlight.call(self, content);
                } else if (_.settings.code_highlight === 'rainbow') {
                    try {
                        Rainbow.color(content[0]);
                    } catch (e) {
                    }
                } else if (_.settings.code_highlight === 'prism') {
                    try {
                        content.find('pre[class*="language-"],code[class*="language-"]').each(function () {
                            Prism.highlightElement(this);
                        })
                    } catch (e) {
                    }
                }


                //wrap titles and contents inside panels
                if (_.settings.add_panels) {
                    content
                        .find(_.settings.panel_content_selector).each(function () {
                        var header = $(this).prevAll(_.settings.panel_content_title);
                        if (header.length == 0) return false;

                        header =
                            header.attr('class', 'panel-title')
                                .wrapInner('<a class="help-panel-toggle" href="#" data-parent="#" data-toggle="collapse" />')
                                .wrap('<div class="panel-heading" />')
                                .closest('.panel-heading');

                        $(this).wrap('<div class="panel panel-default panel-help"><div class="panel-collapse collapse"><div class="panel-body"></div></div></div>');
                        $(this).closest('.panel').prepend(header);
                    })

                    var group_count = $('.panel-group').length;
                    content.find('.panel').each(function () {
                        if ($(this).parent().hasClass('panel-group')) return;

                        var group_id = 'panel-group-help-' + (++group_count);
                        var group = $('<div class="panel-group" />').insertBefore(this);
                        group.attr('id', group_id);

                        var panel_id = 0;
                        group.siblings('.panel').appendTo(group);
                        group.find('.help-panel-toggle')
                            .append('<i class="pull-right ace-icon fa fa-plus" data-icon-show="ace-icon fa fa-plus" data-icon-hide="ace-icon fa fa-minus"></i>')
                            .attr('data-parent', '#' + group_id)
                            .each(function () {
                                panel_id++;
                                $(this).attr('data-target', '#' + group_id + '-' + panel_id);
                                $(this).closest('.panel-heading').siblings('.panel-collapse').attr('id', group_id + '-' + panel_id);
                            });
                    });
                    $(document).off('click.help-panel-toggle', '.help-panel-toggle').on('click.help-panel-toggle', '.help-panel-toggle', function (e) {
                        e.preventDefault();
                    });
                }


                ///////////////////////////////////////////

                content.removeClass('hidden')

                var images = content.find('img:visible');
                if (images.length > 0) {
                    //handle scrollbars when all images are loaded
                    var ev_count = 0;
                    images.off('.help_body_scroll').on('load.help_body_scroll error.help_body_scroll', function () {
                        $(this).off('.help_body_scroll');
                        ev_count++;
                        if (ev_count >= images.length) disableBodyScroll();
                    });
                }

                disableBodyScroll();
                content.find('.panel > .panel-collapse').on('shown.bs.collapse hidden.bs.collapse', function () {
                    disableBodyScroll();
                });


                //save history list
                add_to_nav_list(section_name, save_to_list);

                var pos = -1;
                if ((pos = section_name.lastIndexOf('.')) > -1) {
                    section_name = section_name.substr(0, pos);
                    help_modal.find('button[data-navdir=up]').removeClass('disabled').attr('data-url', section_name);
                } else {
                    help_modal.find('button[data-navdir=up]').addClass('disabled').removeAttr('data-url').blur();
                }
            })
            .fail(function () {
                help_modal.find('.modal-title').find('.fa-spin').remove().end().find('.hidden').children().unwrap();
            });
    }//launch_help_modal


    $(document).on('click', '.onpage-help-modal a[href^="http"]', function () {
        $(this).attr('target', '_blank');
    });

    $(document).on('click', '.help-more', function (e) {
        e.preventDefault();
        var href = $(this).attr('href');
        launch_help_modal(href);
    });


    function add_to_nav_list(section_name, save_to_list) {
        if (save_to_list !== false) {
            if (nav_list.length > 0) {
                nav_list = nav_list.slice(0, nav_pos + 1);
            }
            if (nav_list[nav_list.length - 1] != section_name) {
                nav_list.push(section_name);
                nav_pos = nav_list.length - 1;
            }
        }

        if (nav_pos == 0) {
            help_modal.find('button[data-navdir=back]').addClass('disabled').blur();
        } else {
            help_modal.find('button[data-navdir=back]').removeClass('disabled');
        }

        if (nav_pos == nav_list.length - 1) {
            help_modal.find('button[data-navdir=forward]').addClass('disabled').blur();
        } else {
            help_modal.find('button[data-navdir=forward]').removeClass('disabled');
        }
    }


    $(document).on('click', '.open-file[data-open-file]', function () {
        help_modal.find('.modal-title').wrapInner("<span class='hidden' />").append('<i class="fa fa-spinner fa-spin blue bigger-125"></i>');
        $('.onpage-help-content').addClass('hidden')

        var url = $(this).attr('data-path') || $(this).text();
        var language = $(this).attr('data-open-file');
        display_codeview(url, language, true);
    });


    function display_codeview(url, language, save_to_list) {
        var $url = url;

        if (typeof _.settings.file_url === 'function') url = _.settings.file_url.call(self, url, language);
        $.ajax({url: url, dataType: 'text'})
            .done(function (result) {

                add_to_nav_list('file:' + language + ':' + $url, save_to_list);

                help_modal.find('button[data-navdir=up]').addClass('disabled').blur();
                help_modal.find('.modal-title').html($url).wrapInner('<code />');

                if (language != 'json') {
                    if (language != 'css') {
                        //replace each tab character with two spaces (only those that start at a new line)
                        result = result.replace(/\n[\t]{1,}/g, function (p, q) {
                            return p.replace(/\t/g, "  ");
                        });
                    } else {
                        result = result.replace(/\t/g, "  ")
                    }
                } else {
                    language = 'javascript';
                    result = JSON.stringify(JSON.parse(result), null, 2);//add spacing and somehow beautification
                }

                result = result.replace(/\>/g, '&gt;').replace(/\</g, '&lt;');

                var content = $('.onpage-help-content');
                content.removeClass('hidden').empty();

                if (typeof _.settings.code_highlight === 'function') {
                    result = _.settings.code_highlight.call(self, result, language);
                    content.html(result);
                } else if (_.settings.code_highlight === 'rainbow') {
                    try {
                        Rainbow.color(result, language, function (highlighted_code) {
                            content.html(highlighted_code).wrapInner('<pre data-language="' + language + '" />');
                        });
                    } catch (e) {
                    }
                } else if (_.settings.code_highlight === 'prism') {
                    try {
                        result = Prism.highlight(result, Prism.languages[language], language);
                        content.html(result).wrapInner('<pre class="language-' + language + '" />');
                    } catch (e) {
                    }
                }

            });
    }

}
